Option Explicit

'################################################################
' Descriptor
' sql token 提取器
'################################################################

' sql 字符串分割器
Private sqlStrExtractor As StringTokenExtractor

' 解析时，是否需要保留全部的 token
Private needAllTokens As Boolean

' token 缓存
' ----------------------------------------
' 单一sql解析结果
' ----------------------------------------
' 保存所有的 token
' IObjArrayRef<SqlToken>
Private allTokenCache As IObjArrayRef

' 只保存有意义的 token
' IObjArrayRef<SqlToken>
Private meanTokenCache As IObjArrayRef

' ----------------------------------------
' 多sql解析结果
' ----------------------------------------
' 按顺序记录各个 sql 的全部 token
' IObjArrayRef<IObjArrayRef<SqlToken>>
Private multiAllTokenCache As IObjArrayRef

' 按顺序记录各个 sql 的 token，只保留有意义的 token
' IObjArrayRef<IObjArrayRef<SqlToken>>
Private multiMeanTokenCache As IObjArrayRef

' ----------------------------------------
' 配置属性
' ----------------------------------------
' sql 中的注释是否作为有意义的 token
' 默认设置为无意义 token
Private sqlCommentIsMeanToken As Boolean


'################################################################
' Init
'################################################################
Public Sub Init(Optional ByRef needAllTokensVal As Boolean = True)
    ' ==================================================
    ' 1. 初始化逻辑
    ' ==================================================
    ' 1. 初始化: sql 字符串分割器
    initSqlStrExtractor

    ' 2. 保存配置内容: 解析 sql 时，是否需要保留所有的 token
    needAllTokens = needAllTokensVal

    ' ==================================================
    ' 2. 属性的默认初始化
    ' ==================================================
    ' sql 中的注释是否作为有意义的 token
    sqlCommentIsMeanToken = False
End Sub

Private Sub initSqlStrExtractor()
    ' 1. 初始化: sql 字符串分割器
    Set sqlStrExtractor = New StringTokenExtractor
    ' 2. 需要保留的分割字符串
    sqlStrExtractor.addMultiRetainableDelimiter Array( _
        " ", vbTab, vbCrLf, vbLf, _
        "`", ",", ".", ";", _
        "(", ")", _
        "=", _
        "!=", "<>", _
        "<", "&lt;", _
        ">", "&gt;", _
        "<=", "&lt;=", _
        ">=", "&gt;=", _
        "||", "+", "-", "*", "/")

    ' 3. 添加不做分割的包围字符串对
    ' part1--纯字符串: key1=`"'"`, key2=`"'"`
    sqlStrExtractor.addSurroundPair "'", "'"
    ' part2--注释: key1=`"--"`, key2=`"\r\n"`
    sqlStrExtractor.addSurroundPair "--", vbCrLF
    ' part3--注释: key1=`"--"`, key2=`"\n"`
    sqlStrExtractor.addSurroundPair "--", vbLF
    ' part4--多行注释: key1=`"/*"`, key2=`"*/`
    sqlStrExtractor.addSurroundPair "/*", "*/"
    ' part5--mybatis参数: key1=`"#{"`, key2=`"}"`
    sqlStrExtractor.addSurroundPair "#{", "}"
    ' part6--mybatis参数: key1=`"${"`, key2=`"}"`
    sqlStrExtractor.addSurroundPair "${", "}"
End Sub

'################################################################
' Property
'################################################################

' 获取内置的字符串解析器，以支持外部的自定义解析
Public Property Get innerStrExtractor() As StringTokenExtractor
    Set innerStrExtractor = sqlStrExtractor
End Property

' --------------------------------------------------
' 获取单一sql解析结果
' --------------------------------------------------
' 获取解析 sql 后生成的: `有意义 token 数组引用`
Public Property Get meaningfulTokens() As IObjArrayRef
    Set meaningfulTokens = meanTokenCache
End Property

' 获取解析 sql 后生成的: `全部 token 的数组引用`
Public Property Get allTokens() As IObjArrayRef
    Set allTokens = allTokenCache
End Property

' --------------------------------------------------
' 获取多sql解析结果
' --------------------------------------------------
' 获取解析 sql 后生成的: `有意义 token 数组引用`
Public Property Get multiMeaningfulTokens() As IObjArrayRef
    Set multiMeaningfulTokens = multiMeanTokenCache
End Property

' 获取解析 sql 后生成的: `全部 token 的数组引用`
Public Property Get multiAllTokens() As IObjArrayRef
    Set multiAllTokens = multiAllTokenCache
End Property

'################################################################
' Function
'################################################################
' ---------------------------------------------
' 配置方法
' ---------------------------------------------
' 设置注释是否作为有意义token
Public Sub setSqlCommentIsMeanToken(ByRef v As Boolean)
    sqlCommentIsMeanToken = v
End Sub

' ---------------------------------------------
' 解析分词方法
' ---------------------------------------------
' 执行解析
' @param sql 需要分词的 sql
Public Sub analyze(ByRef sql As String)
    ' 重新初始化 token 缓存
    Set meanTokenCache = UtilsArrayRef.createRealObjArrayRef(10)

    If needAllTokens Then
        Set allTokenCache = UtilsArrayRef.createRealObjArrayRef(10)
    End If

    ' 如果当前 sql 字符串是空字符串，则退出
    If sql = "" Then
        Exit Sub
    End If

    ' 解析 sql
    sqlStrExtractor.analyze sql

    Dim word As String
    Dim lcaseWord As String
    Dim currentToken As SqlToken
    Dim tokenType As SqlTokenType

    ' 用来记录上一个 token 是不是换行符
    ' 初始化为 true，用来标记: 如果第一个 token 是注释，则这个注释是单独一行的
    Dim lastCharIsLineBreak As Boolean
    lastCharIsLineBreak = True
    While sqlStrExtractor.hasToken
        word = sqlStrExtractor.getNextToken
        lcaseWord = LCase(word)

        If SqlHelper.isSqlKey(lcaseWord) Then
            lastCharIsLineBreak = False

            ' 1. 处理关键字(小写进行比较)
            Set currentToken = New SqlToken
            tokenType = SqlHelper.getSqlKeyTokenType(lcaseWord)
            currentToken.Init word, tokenType

            ' 将 token 添加到缓存
            If needAllTokens Then
                allTokenCache.Add currentToken
            End If

            meanTokenCache.Add currentToken
        ElseIf parseMultiWordSqlKey(word, lcaseword) Then
            lastCharIsLineBreak = False

            ' 2. 处理多单词关键字
            ' noop
        ElseIf SqlHelper.isMeaningfulDelimiterStr(word) Then
            lastCharIsLineBreak = False

            ' 3. 处理有意义分隔符
            scanSqlMeaningfulDelimiterToken word
        ElseIf wordIsStr(word) Then
            lastCharIsLineBreak = False

            ' 4. 处理普通字符串
            Set currentToken = new SqlToken
            currentToken.Init word, SqlTokenType.STR_STR

            ' 将 token 添加到缓存
            If needAllTokens Then
                allTokenCache.Add currentToken
            End If
            meanTokenCache.Add currentToken
        ElseIf wordIsMeanComment(word) Then
            ' 5. 处理注释(当注释时有意义 token 时)
            scanSqlMeanComment word, lastCharIsLineBreak

            ' 注释的末尾有两种情况: 1=换行符 2=sql结束
            ' 所以无论是哪种情况，这里全部设置为 True
            lastCharIsLineBreak = True
        ElseIf wordIsNoMeanToken(word) Then
            ' 6. 处理无意义内容
            scanSqlNoMeanToken word

            ' 检查: 如果 word 是换行符，则设置换行符状态
            If word = vbCrLf Then
                lastCharIsLineBreak = true
            ElseIf word = vbLf Then
                lastCharIsLineBreak = true

            ' 如果是空格，则跳过。用来维持之前的换行符状态
            ElseIf word = " " Then
                ' noop

            Else
                lastCharIsLineBreak = false
            End If
        ElseIf UtilsString.isNumStr(word) Then
            lastCharIsLineBreak = False

            ' 7. 处理数字
            scanSqlNumToken word
        ElseIf SqlHelper.isSqlConst(word) Then
            lastCharIsLineBreak = False

            ' 8. 处理 sql 常量
            scanSqlConstToken word, lcaseWord
        Else
            lastCharIsLineBreak = False

            ' 9. 处理非全数字字符串
            scanSqlNameToken word
        End If
    Wend
End Sub

' ---------------------------------------------
' 多 sql 的解析实现
' ---------------------------------------------
' 执行解析
' @param sql 需要分词的 sql
Public Sub analyzeMultiSql(ByRef sql As String, Optional ByRef needSemicolon As Boolean = True)
    ' 重新初始化 token 缓存
    Set multiMeanTokenCache = UtilsArrayRef.createRealObjArrayRef(10)

    If needAllTokens Then
        Set multiAllTokenCache = UtilsArrayRef.createRealObjArrayRef(10)
    End If

    ' 如果当前 sql 字符串是空字符串，则退出
    If sql = "" Then
        Exit Sub
    End If


    ' 先创建一个 sql 的缓存
    Set meanTokenCache = UtilsArrayRef.createRealObjArrayRef
    If needAllTokens Then
        Set allTokenCache = UtilsArrayRef.createRealObjArrayRef
    End If

    ' 解析 sql
    sqlStrExtractor.analyze sql

    Dim word As String
    Dim lcaseWord As String
    Dim currentToken As SqlToken
    Dim tokenType As SqlTokenType

    ' 用来记录上一个 token 是不是换行符
    ' 初始化为 true，用来标记: 如果第一个 token 是注释，则这个注释是单独一行的
    Dim lastCharIsLineBreak As Boolean
    lastCharIsLineBreak = True

    While sqlStrExtractor.hasToken
        word = sqlStrExtractor.getNextToken
        lcaseWord = LCase(word)

        ' 如果是分号，则结束当前 sql 的解析，缓存这个 sql 的解析结果，然后创建下一个 sql 的缓存
        If word = ";" Then
            lastCharIsLineBreak = False

            If needSemicolon Then
                ' 保存 ;
                Set currentToken = New SqlToken
                currentToken.Init word, SqlTokenType.SEMICOLON
                meanTokenCache.Add currentToken

                If needAllTokens Then
                    allTokenCache.Add currentToken
                End If
            End If

            ' 保存这一组的 sql
            multiMeanTokenCache.Add meanTokenCache
            Set meanTokenCache = UtilsArrayRef.createRealObjArrayRef

            If needAllTokens Then
                multiAllTokenCache.Add allTokenCache
                Set allTokenCache = UtilsArrayRef.createRealObjArrayRef
            End If

            GoTo whileContinue
        End If

        If SqlHelper.isSqlKey(lcaseWord) Then
            lastCharIsLineBreak = False

            ' 1. 处理关键字(小写进行比较)
            Set currentToken = New SqlToken
            tokenType = SqlHelper.getSqlKeyTokenType(lcaseWord)
            currentToken.Init word, tokenType

            ' 将 token 添加到缓存
            If needAllTokens Then
                allTokenCache.Add currentToken
            End If

            meanTokenCache.Add currentToken
        ElseIf parseMultiWordSqlKey(word, lcaseword) Then
            lastCharIsLineBreak = False

            ' 2. 处理多单词关键字
            ' noop
        ElseIf SqlHelper.isMeaningfulDelimiterStr(word) Then
            lastCharIsLineBreak = False

            ' 3. 处理有意义分隔符
            scanSqlMeaningfulDelimiterToken word
        ElseIf wordIsStr(word) Then
            lastCharIsLineBreak = False

            ' 4. 处理普通字符串
            Set currentToken = new SqlToken
            currentToken.Init word, SqlTokenType.STR_STR

            ' 将 token 添加到缓存
            If needAllTokens Then
                allTokenCache.Add currentToken
            End If
            meanTokenCache.Add currentToken
        ElseIf wordIsMeanComment(word) Then
            ' 5. 处理注释(当注释时有意义 token 时)
            scanSqlMeanComment word, lastCharIsLineBreak

            ' 注释的末尾有两种情况: 1=换行符 2=sql结束
            ' 所以无论是哪种情况，这里全部设置为 True
            lastCharIsLineBreak = True
        ElseIf wordIsNoMeanToken(word) Then
            ' 6. 处理无意义内容
            scanSqlNoMeanToken word

            ' 检查: 如果 word 是换行符，则设置换行符状态
            If word = vbCrLf Then
                lastCharIsLineBreak = true
            ElseIf word = vbLf Then
                lastCharIsLineBreak = true

            ' 如果是空格，则跳过。用来维持之前的换行符状态
            ElseIf word = " " Then
                ' noop

            Else
                lastCharIsLineBreak = false
            End If
        ElseIf UtilsString.isNumStr(word) Then
            lastCharIsLineBreak = False

            ' 7. 处理数字
            scanSqlNumToken word
        ElseIf SqlHelper.isSqlConst(word) Then
            lastCharIsLineBreak = False

            ' 8. 处理 sql 常量
            scanSqlConstToken word, lcaseWord
        Else
            lastCharIsLineBreak = False

            ' 9. 处理非全数字字符串
            scanSqlNameToken word
        End If
whileContinue:
    Wend

    ' 保存结果
    If meanTokenCache.Count > 0 Then
        multiMeanTokenCache.Add meanTokenCache
    End If

    If needAllTokens Then
        If allTokenCache.Count > 0 Then
            multiAllTokenCache.Add allTokenCache
        End If
    End If

    ' 清空单一 sql 的缓存
    Set meanTokenCache = Nothing
    Set allTokenCache = Nothing
End Sub

' ---------------------------------------------
' 检查方法
' ---------------------------------------------
' 检查 word 是不是无意义内容
Public Function isNoMeanStr(ByRef s As String) As Boolean
    isNoMeanStr = wordIsNoMeanToken(s)
End Function

' 检查 word 是不是数字
Public Function isNumStr(ByRef s As String) As Boolean
    isNumStr = UtilsString.isNumStr(s)
End Function

' 检查 word 是不是字符串
Public Function isStr(ByRef s As String) As Boolean
    isStr = wordIsStr(s)
End Function

'################################################################
' Private Function
'################################################################

' 检查 word 是不是一个字符串
' 特殊情况:
'      对于: mybatis参数，这里也当作字符串
Private Function wordIsStr(ByRef word As String) As Boolean
    Dim firstChar As String
    Dim lastChar As String
    firstChar = Left(word, 1)
    lastChar = Right(word, 1)

    If firstChar = "'" And lastChar = "'" Then
        ' ''
        wordIsStr = True
    Else
        ' #{}
        ' ${}
        If lastChar = "}" Then
            firstChar = Left(word, 2)
            If firstChar = "#{" Then
                wordIsStr = True
            ElseIf firstChar = "#{" Then
                wordIsStr = True
            Else
                wordIsStr = False
            End If
        Else
            wordIsStr = False
        End If
    End If
End Function

' 处理注释(当注释时有意义 token 时)
Private Function wordIsMeanComment(ByRef word As String) As Boolean
    ' 如果当前不会将注释作为有意义 token，则退出并返回 false
    If sqlCommentIsMeanToken = False Then
        wordIsMeanComment = False
        Exit Function
    End If

    ' 如果是 -- 开头，则作为单行注释处理
    Dim temp As String
    temp = Left(word, 2)
    If temp = "--" Then
        ' return true
        wordIsMeanComment = True
        Exit Function
    End If

    ' 多行注释
    If temp = "/*" Then
        If Right(word, 2) = "*/" Then
            ' return true
            wordIsMeanComment = True
            Exit Function
        End If
    End If

    ' return false
End Function

' 检查 word 是不是无意义内容
Private Function wordIsNoMeanToken(ByRef word As String) As Boolean
    ' 1. 如果在无意义字符集中，返回 true
    If SqlHelper.isSqlNoMeanStr(word) Then
        wordIsNoMeanToken = True
        Exit Function
    End If

    ' 2. 如果是注释，并且注释是无意义 token，则是无意义内容
    If sqlCommentIsMeanToken = False Then
        ' 单行注释
        Dim temp As String
        temp = Left(word, 2)
        If temp = "--" Then
            wordIsNoMeanToken = True
            Exit Function
        End If

        ' 多行注释
        If temp = "/*" Then
            If Right(word, 2) = "*/" Then
                wordIsNoMeanToken = True
                Exit Function
            End If
        End If
    End If

    ' 3. [`]，进行忽略
    If word = "`" Then
        wordIsNoMeanToken = True
        Exit Function
    End If

    wordIsNoMeanToken = False
End Function

' 尝试匹配多单词关键字: group by + order by
Private Function parseMultiWordSqlKey(ByRef word As String, ByRef lcaseword As String) As Boolean
    parseMultiWordSqlKey = False

    ' 1. 如果当前 word 不是 group + order，返回 false 结束
    If lcaseword <> "group" And lcaseword <> "order" Then
        'return false
        Exit Function
    End If

    ' 2. 向后搜索，首先过滤掉所有的空格
    Dim temp As String
    Dim noMeanStr As String
    Dim comments As IPrimitiveArrayRef
    While sqlStrExtractor.hasToken
        temp = sqlStrExtractor.getNextToken

        If wordIsMeanComment(temp) Then
            ' 处理注释(当注释时有意义 token 时)，需要将注释单独保存
            If comments Is Nothing Then
                Set comments = UtilsArrayRef.createRealPrimitiveArrayRef(2)
            End If

            comments.Add temp
        ElseIf wordIsNoMeanToken(temp) Then
            ' 如果是空格，则需要记录，在最后保存成无意义 token
            noMeanStr = noMeanStr & temp
        Else
            ' 把这个有意义的 token 重新放回，来防止出发下面的分词器用完的判断
            sqlStrExtractor.putBack temp
            GoTo outNoMeanBreak
        End If
    Wend
outNoMeanBreak:

    If Not sqlStrExtractor.hasToken Then
        ' 回退无意义的 token + 注释
        backNoSqlStr noMeanStr, comments
        ' 回退分词器
        sqlStrExtractor.putBack temp
        'return false
        Exit Function
    End If

    ' 3. 检查下一个 token，如果不是 by，则回退分词器，并返回 false
    temp = sqlStrExtractor.getNextToken
    If LCase(temp) <> "by" Then
        ' 回退无意义的 token + 注释
        backNoSqlStr noMeanStr, comments
        ' 回退分词器
        sqlStrExtractor.putBack temp
        'return false
        Exit Function
    End If

    ' 4. 直接在这里保存 token
    ' 将 group/order + 无意义字符 + by 统一保存成一个 sql 关键字 token
    '                  无意义字符 --> 一个空格
    Dim currentToken As SqlToken
    If lcaseword = "group" Then
        Set currentToken = New SqlToken
        ' currentToken.Init word & tempBackup & temp, SqlTokenType.KEY_GROUP_BY
        currentToken.Init word & " " & temp, SqlTokenType.KEY_GROUP_BY
    Else
        Set currentToken = New SqlToken
        ' currentToken.Init word & tempBackup & temp, SqlTokenType.KEY_ORDER_BY
        currentToken.Init word & " " & temp, SqlTokenType.KEY_ORDER_BY
    End If

    ' 5. 保存当前的多 key 组合 token
    If needAllTokens Then
        allTokenCache.Add currentToken
    End If
    meanTokenCache.Add currentToken

    ' 6. 尝试保存注释
    '    这里先保存注释，全部保存到组合关键字的下一行
    If Not comments Is Nothing Then
        Dim commentsMaxIdx As Long
        Dim i As Long
        commentsMaxIdx = comments.MaxIndex

        For i = 0 To commentsMaxIdx
            scanSqlMeanComment comments.Item(i), True
        Next
    End If

    ' 这里不再保存无意义字符，即:无论多个 key 之间有多少的无意义字符，全部忽略，最终设置为一个空格
    ' 7. 保存无意义字符
    ' If noMeanStr <> "" Then
    '     If needAllTokens Then
    '         Set currentToken = New SqlToken
    '         currentToken.Init noMeanStr, SqlTokenType.NO_MEAN
    '         allTokenCache.Add currentToken
    '     End If
    ' End If

    parseMultiWordSqlKey = True
End Function

' 回退非 sql 的字符串
' 包括无意义 token + 注释
' 先回退无意义 token，再回退注释，这样在重新获取时，将会先得到注释，后得到无意义 token
' @param noMeanStr 无意义 token
' @param comments 注释 list
Private Sub backNoSqlStr(ByRef noMeanStr As String, ByRef comments As IPrimitiveArrayRef)
    ' 回退无意义 token
    If noMeanStr <> "" Then
        sqlStrExtractor.putBack noMeanStr
    End If

    ' 回退注释，需要从后向前保存，这样获取时才能保持顺序
    If Not comments Is Nothing Then
        Dim i As Long
        For i = comments.MaxIndex To 0 Step -1
            sqlStrExtractor.putBack comments.Item(i)
        Next
    End If
End Sub

' 处理有意义分隔符
Private Sub scanSqlMeaningfulDelimiterToken(ByRef word As String)
    Dim currentToken As SqlToken
    If word <> "*" Then
        ' 如果不是 *, 则直接获取分割符对应的 tokenType，然后包装成 token
        Set currentToken = New SqlToken
        currentToken.Init word, SqlHelper.getTokenTypeForMeaningfulDelimiter(word)

        ' 将 token 添加到缓存
        If needAllTokens Then
            allTokenCache.Add currentToken
        End If
        meanTokenCache.Add currentToken
    Else
        ' 检查当前的 * 是[全选择符号]还是[乘法符号]

        ' 1. 检查前一个有意义 token，如果是 [.]，则当前符号是全选择符号
        Dim lastToken As SqlToken
        Set lastToken = meanTokenCache.getLast
        If Not lastToken Is Nothing Then
            If lastToken.tokenType = SqlTokenType.POINT Then
                Set currentToken = New SqlToken
                currentToken.Init "*", SqlTokenType.ASTERISK

                ' 将 token 添加到缓存
                If needAllTokens Then
                    allTokenCache.Add currentToken
                End If
                meanTokenCache.Add currentToken

                Exit Sub
            End If
        End If

        ' 2. 检查 * 后面的 token 来确定 * 的类型
        Dim temp As String
        Dim componentStack As PrimitiveStack

        temp = ""

        ' 过滤 * 后面的无意义内容
        While sqlStrExtractor.hasToken
            temp = sqlStrExtractor.getNextToken

            If wordIsMeanComment(temp) Then
                ' 处理注释(当注释时有意义 token 时)，需要将注释单独保存
                If componentStack Is Nothing Then
                    Set componentStack = New PrimitiveStack
                End If
                componentStack.push temp
            ElseIf wordIsNoMeanToken(temp) Then
                ' 处理无意义 token
                If componentStack Is Nothing Then
                    Set componentStack = New PrimitiveStack
                End If
                componentStack.push temp
            Else
                sqlStrExtractor.putBack temp
                Goto while1Break
            End If
        Wend
while1Break:

        Dim isAsterisk As Boolean
        isAsterisk = False
        If sqlStrExtractor.hasToken Then
            ' 这里只是为了检验下一个有意义、非注释 token 的类型
            ' 所以取出后，立刻放回（回退）
            temp = sqlStrExtractor.getNextToken
            If componentStack Is Nothing Then
                sqlStrExtractor.putBack temp
            Else
                componentStack.push temp
            End If

            ' 如果是以下几种情况，则这个 * 是全列选择符号
            '    `,`, `)`, `;`
            '    `case`, `null` 以外的 sql 关键字

            If temp = "," Then
                isAsterisk = True
            ElseIf temp = ")" Then
                isAsterisk = True
            ElseIf temp = ";" Then
                isAsterisk = True
            Else
                temp = LCase(temp)

                If SqlHelper.isSqlKey(temp) _
                    And temp <> "case" And temp <> "null" _
                    And temp <> "false" And temp <> "true" Then

                    isAsterisk = True
                End If
            End If
        End If

        ' 3. 创建对应类型的 token 对象，并添加到缓存
        If isAsterisk Then
            Set currentToken = New SqlToken
            currentToken.Init "*", SqlTokenType.ASTERISK
        Else
            Set currentToken = New SqlToken
            currentToken.Init "*", SqlTokenType.STR_STR
        End If

        If needAllTokens Then
            allTokenCache.Add currentToken
        End If
        meanTokenCache.Add currentToken

        ' 4. 将栈中的元素回退
        If Not componentStack Is Nothing Then
            backStackToExtractor componentStack
        End If
    End If
End Sub

' 处理（有意义的）sql 注释
Private Sub scanSqlMeanComment(ByRef word As String, ByRef lastCharIsLineBreak As Boolean)
    ' 吸收 -- 开始的单行注释
    If Left(word, 2) = "--" Then
        doSaveOneLineComment word, lastCharIsLineBreak

    ' 吸收 /* 开头的单行注释
    Else
        doSaveCommentFromMultiLineMark word, lastCharIsLineBreak
    End If
End Sub

' 吸收 -- 开始的单行注释
' 或者由 /* */ 包裹的、内部没有换行符的单行注释
Private Sub doSaveOneLineComment(ByRef word As String, ByRef lastCharIsLineBreak As Boolean)
    ' 1. 创建 token
    Dim currentToken As SqlToken
    Set currentToken = New SqlToken

    If lastCharIsLineBreak Then
        ' 如果前一个 token 是换行符，则需要设置为换行后的注释
        currentToken.Init UtilsString.deleteStrEndLineBreaks(word), SqlTokenType.SINGLE_LINE_COMMENT
    Else
        currentToken.Init UtilsString.deleteStrEndLineBreaks(word), SqlTokenType.SHARE_LINE_COMMENT
    End If

    ' 2. 将 token 添加到缓存
    meanTokenCache.Add currentToken
    If needAllTokens Then
        allTokenCache.Add currentToken

        ' 检查，如果 token 中保存的数据长度 < word 的长度，则说明存在换行符
        ' 需要将换行保存到缓存
        Dim lineBreakSize As Integer
        lineBreakSize = Len(word) - Len(currentToken.value)
        If lineBreakSize > 0 Then
            Set currentToken = New SqlToken
            currentToken.Init Right(word, lineBreakSize), SqlTokenType.NO_MEAN
            allTokenCache.Add currentToken
        End If
    End If
End Sub

' 吸收 /* 开头的单行或多行注释
Private Sub doSaveCommentFromMultiLineMark(ByRef word As String, ByRef lastCharIsLineBreak As Boolean)
    ' ==================================================
    ' 1. 检查: 如果没有换行符，则作为单行注释来吸收，并退出处理
    ' ==================================================
    '    因为只处理两种换行符，`\r\n`, `\n` 所以这里只检查 `\n`
    '    这里只检查 `\n`
    If InStr(word, vbLf) = 0 Then
        doSaveOneLineComment word, lastCharIsLineBreak
        Exit Sub
    End If

    ' ==================================================
    ' 2. 用 `\n` 分割注释字符串
    ' ==================================================
    Dim splitor() As String
    splitor = Split(word, VbLf)

    ' ==================================================
    ' 3. 第一行注释按照单行注释来吸收
    ' ==================================================
    '    保持第一行注释的: 和前一个元素是同行的状态，或者单独一行的状态
    '    这里需要在末尾手动添加 \n 以保证换行符的完整
    doSaveOneLineComment splitor(0) & vbLf, lastCharIsLineBreak

    ' ==================================================
    ' 4. 如果不需要保存所有token needAllTokens == false
    ' ==================================================
    Dim i As Integer
    Dim temp As String
    Dim token As SqlToken

    If needAllTokens = False Then
        ' 1. 从 i = 1 开始到最后一行，遍历每一行注释
        For i = 1 To UBound(splitor)
            ' 2. 清除这行注释左右两侧的空格
            temp = trim(splitor(i))

            ' 3. 创建元素，并保存
            '    保存时，在前面额外附加一个空格用来保持多行注释的格式
            Set token = New SqlToken
            token.Init " " & UtilsString.deleteStrEndLineBreaks(temp), SqlTokenType.SINGLE_LINE_COMMENT
            meanTokenCache.Add token
        Next

    ' ==================================================
    ' 5. 如果需要保存所有token needAllTokens == true
    ' ==================================================
    Else
        Dim startSpaceCount As Integer
        ' 换行符的字符串
        Dim breakLineStr As String
        Dim maxIndex As Integer
        maxIndex = UBound(splitor)

        ' 1. 从 i = 1 开始到倒数第二行，遍历每一行注释
        For i = 1 To maxIndex - 1
            ' 获取这一行注释
            temp = splitor(i)

            ' 2. 处理注释前面的空格
            ' 2.1 计算注释开头的空格数量 = 获取第一个不是空格的位置 - 2
            '     - 2 表示: 1 (第一个非空格元素的长度) + 1 (为了保持注释格式而保存到 token 内的空格)
            startSpaceCount = UtilsString.findFirstNoTargetIndex(temp, " ") - 2
            ' 2.2 检查: 如果空格数量 > 0，则创建等量的空格，并保存为无意义 token
            If startSpaceCount > 0 Then
                Set token = New SqlToken
                token.Init space(startSpaceCount), SqlTokenType.NO_MEAN

                allTokenCache.Add token
            End If

            ' 3. 确认需要保存的注释内容 + 换行符
            ' 3.1 检查: 如果最后一个字符是 `\r`
            If Right(temp, 1) = vbCr Then
                ' 去除最后一个字符
                temp = Left(temp, Len(temp) - 1)
                breakLineStr = vbCrLf

            ' 3.2. 处理 \n 换行的情况
            Else
                ' 注释内容不变
                breakLineStr = vbLf
            End If

            ' 4. 保存注释本身
            '     需要去除注释左右两侧的空格 + 在前面额外附加一个空格用来保持多行注释的格式
            Set token = New SqlToken
            token.Init " " & trim(temp), SqlTokenType.SINGLE_LINE_COMMENT

            meanTokenCache.Add token
            allTokenCache.Add token

            ' 5. 保存换行符
            Set token = New SqlToken
            token.Init breakLineStr, SqlTokenType.NO_MEAN

            allTokenCache.Add token
        Next

        ' 6. 处理最后一行注释 */ 。因为最后一行没有换行符，所以单独处理
        ' 6.1 获取最后一行注释
        temp = splitor(maxIndex)

        ' 6.2 处理注释前面的空格
        ' 6.2.1 计算注释开头的空格数量 = 获取第一个不是空格的位置 - 2
        '     - 2 表示: 1 (第一个非空格元素的长度) + 1 (为了保持注释格式而保存到 token 内的空格)
        startSpaceCount = UtilsString.findFirstNoTargetIndex(temp, " ") - 2
        ' 6.2.2 检查: 如果空格数量 > 0，则创建等量的空格，并保存为无意义 token
        If startSpaceCount > 0 Then
            Set token = New SqlToken
            token.Init space(startSpaceCount), SqlTokenType.NO_MEAN

            allTokenCache.Add token
        End If

        ' 6.3 处理注释本身: 创建元素，并保存
        '    需要去除注释左右两侧的空格 + 在前面额外附加一个空格用来保持多行注释的格式
        Set token = New SqlToken
        token.Init " " & trim(temp), SqlTokenType.SINGLE_LINE_COMMENT

        meanTokenCache.Add token
        allTokenCache.Add token
    End If
End Sub

' 处理无意义内容
Private Sub scanSqlNoMeanToken(ByRef word As String)
    ' 如果不需要保存全部 token，则跳过
    If needAllTokens = False Then
        Exit Sub
    End If

    Dim lastToken As SqlToken
    Set lastToken = allTokenCache.getLast

    Dim currentToken As SqlToken
    If lastToken Is Nothing Then
        ' 1. 第一次发现无意义内容，则新建一个无意义 token
        Set currentToken = New SqlToken
        currentToken.Init word, SqlTokenType.NO_MEAN
        allTokenCache.Add currentToken
    Else
        If lastToken.tokenType = SqlTokenType.NO_MEAN Then
            ' 2. 如果前一个 token 也是无意义内容，则进行合并
            lastToken.value = lastToken.value & word
        Else
            ' 3. 如果前一个 token 不是无意义内容，则新建一个无意义 token
            Set currentToken = New SqlToken
            currentToken.Init word, SqlTokenType.NO_MEAN
            allTokenCache.Add currentToken
        End If
    End If
End Sub

' 处理数字（整数 + 小数） token
' 对于小数 token，按照正常的 sql 语法，小数点[.] 两侧是一定不会有无意义字符的
Private Sub scanSqlNumToken(ByRef word As String)
    ' 1. 处理小数点
    If sqlStrExtractor.hasToken Then
        ' 继续检查下一个 token 是不是一个小数 .
        Dim nextWord As String
        nextWord = sqlStrExtractor.getNextToken

        If nextWord = "." Then
            word = word & "."

            If sqlStrExtractor.hasToken Then
                ' 继续检查 . 后面有没有数字
                nextWord = sqlStrExtractor.getNextToken
                If UtilsString.isNumStr(word) Then
                    word = word & nextWord
                Else
                    ' 如果不是，则放回
                    sqlStrExtractor.putBack nextWord
                End If
            End If
        Else
            ' 如果不是，则放回
            sqlStrExtractor.putBack nextWord
        End If
    End If

    ' 生成数字 token
    Dim currentToken As SqlToken
    Set currentToken = New SqlToken
    currentToken.Init word, SqlTokenType.STR_NUM

    ' 将 token 添加到缓存
    If needAllTokens Then
        allTokenCache.Add currentToken
    End If
    meanTokenCache.Add currentToken
End Sub

' 处理 sql 常量
Private Sub scanSqlConstToken(ByRef word As String, ByRef lcaseWord As String)
    ' 创建 token
    Dim currentToken As SqlToken
    Set currentToken = New SqlToken
    currentToken.Init word, SqlTokenType.getSqlConstType(lcaseWord)

    ' 将 token 添加到缓存
    If needAllTokens Then
        allTokenCache.Add currentToken
    End If
    meanTokenCache.Add currentToken
End Sub

' 这里有两种情况:
' 处理非全数字字符串
'     1. 是一个名称
'     2. 是一个函数名
'         - 后面是一个 [(]，则当前的字符串是函数名
'         - 特殊情况
'             - 如果后面一个是 [(], 前一个有效 token 是关键字 with，则当前的字符串只是一个普通名称
Private Sub scanSqlNameToken(ByRef word As String)
    ' 1. 向后搜索，首先过滤掉所有的空格
    Dim temp As String
    Dim componentStack As PrimitiveStack

    temp = ""

    While sqlStrExtractor.hasToken
        temp = sqlStrExtractor.getNextToken
        If wordIsMeanComment(temp) Then
            ' 处理注释(当注释时有意义 token 时)，需要将注释单独保存
            If componentStack Is Nothing Then
                Set componentStack = New PrimitiveStack
            End If
            componentStack.push temp
        ElseIf wordIsNoMeanToken(temp) Then
            ' 处理无意义 token
            If componentStack Is Nothing Then
                Set componentStack = New PrimitiveStack
            End If
            componentStack.push temp
        Else
            sqlStrExtractor.putBack temp
            Goto while1Break
        End If
    Wend
while1Break:

    ' 2. 如果空格后面是一个 [(], 则表示当前是一个函数名称
    Dim hasLeftParenthesis As boolean
    hasLeftParenthesis = False
    If sqlStrExtractor.hasToken Then
        temp = sqlStrExtractor.getNextToken
        If temp = "(" Then
            ' 处理特殊情况
            ' 前一个有效 token 是关键字 with，则当前的字符串只是一个普通名称
            Dim t As SqlToken
            Set t = getCachedNotCommentToken()
            If Not t Is Nothing Then
                If t.tokenType = SqlTokenType.KEY_WITH Then
                    hasLeftParenthesis = False
                Else
                    hasLeftParenthesis = True
                End If
            Else
                hasLeftParenthesis = True
            End If
        Else
            ' 如果不是 [(], 则将其放回
            hasLeftParenthesis = False
        End If

        ' 回退 token
        If componentStack Is Nothing Then
            sqlStrExtractor.putBack temp
        Else
            componentStack.push temp
        End If
    End If

    ' 3. 保存结果
    Dim currentToken As SqlToken
    If hasLeftParenthesis Then
        ' 如果名称后面有 [(]，则说明当前名称是一个函数名
        Set currentToken = New SqlToken
        currentToken.Init word, SqlTokenType.STR_FUNC
    Else
        ' 这里当作普通名称来保存
        Set currentToken = New SqlToken
        currentToken.Init word, SqlTokenType.STR_NAME
    End If

    If needAllTokens Then
        allTokenCache.Add currentToken
    End If
    meanTokenCache.Add currentToken

    ' 4. 将栈中的元素回退
    If Not componentStack Is Nothing Then
        backStackToExtractor componentStack
    End If
End Sub

' 从 meanTokenCache 中获取上一个非注释的 token
Private Function getCachedNotCommentToken() As SqlToken
    ' 1. 如果缓存中没有任何 token，则返回 null
    If meanTokenCache.Count <= 0 Then
        Exit Function
    End If

    ' 2. 检查: 如果现在的注释是无意义 token，则直接返回最后一个元素
    If sqlCommentIsMeanToken = false Then
        Set getCachedNotCommentToken = meanTokenCache.getLast()
        Exit Function
    End If

    ' 3. 从后向前搜索非注释的 token
    Dim i As Long
    Dim t As SqlToken
    For i = meanTokenCache.MaxIndex To 0 Step -1
        Set t = meanTokenCache.Item(i)

        ' 找到一个非注释 token 后，结束并返回
        If Not SqlHelper.isCommentToken(t.tokenType) Then
            Set getCachedNotCommentToken = t
            Exit Function
        End If
    Next
End Function

' 将栈中的元素回退到分词器中
Private Sub backStackToExtractor(ByRef stack As PrimitiveStack)
    Dim i As Long
    For i = 1 To stack.Count
        sqlStrExtractor.putBack stack.pop
    Next
End Sub